import asyncio
from itertools import cycle
from json.decoder import JSONDecodeError
from message import MSG
from enum import Enum, unique
import json
import logging
from defines import *
from struct import pack, unpack
from typing import List


logger = logging.getLogger()


class CMD(MSG):

    _type_check = None      # Used to check for the correct type in unpack when subclassing
    _subclasses = dict()    # Holds a reference for all defined subclasses msg_id => class
    _handlers = []          # Holds all handlers. MUST initial an empty list for each subclass

    @unique
    class Type(Enum):
        GET_VERSION             = int('10_01', 16)       # 2.1.1
        GET_SERIAL              = int('10_03', 16)       # 2.1.2
        GET_PARA                = int('10_04', 16)       # 2.1.3
        GET_SCAN_PARA           = int('10_05', 16)       # 2.1.4
        GET_CH_CFG              = int('10_06', 16)       # 2.1.5
        SET_SCAN_PARA           = int('20_01', 16)       # 2.2.1
        SET_THRESHOLD           = int('20_02', 16)       # 2.2.2
        SET_AMPLIFY             = int('20_03', 16)       # 2.2.3
        SET_PEAK_INTERVAL       = int('20_04', 16)       # 2.2.4
        SAVE_THRESHOLD          = int('20_06', 16)       # 2.2.5
        SET_IIC                 = int('20_88', 16)       # new for set iic reg directly
        STOP_SAMPLE             = int('30_01', 16)       # 2.3.1
        START_SAMPLE            = int('30_02', 16)       # 2.3.2
        START_TEST              = int('30_03', 16)       # 2.3.3
        START_SPEC_SAMPLE       = int('30_04', 16)       # 2.3.4
        GET_ONESHOT_SPEC        = int('30_07', 16)       # 2.3.5

    

        def __str__(self) -> str:
            return self.name

    @classmethod
    def add_subclass(cls, type_):
        """Decorator for subclasses

        Parameters
        ----------
        type_ : `CMD.Type`
            Indicate the type of cmd subclass.
        """
        def x(subclass):
            subclass._type_check = type_
            cls._subclasses[type_] = subclass
            return subclass
        return x

    def __init__(self, para:dict = None):
        self.type: CMD.Type = self._type_check
        if para is None:
            self.para = b""
        else:
            self.para = para

    def unpack(self, strs):
        """Unpack a json string to cmd class.

        Parameters
        ----------
        strs : `bytes`
            Received `bytes` string. 

        Raises
        ------
        ValueError
            No content in strs.
        TypeError
            Unexpected message type.
        """
        if not(len(strs)):
            raise ValueError("Command unpack wrong : No content in string.")


        cmd_body = strs
        cmd_type, cmd_body_len = unpack("!HB", cmd_body[:3])

        if (len(strs) < (cmd_body_len)):
            raise ValueError("Command unpack wrong : Command length wrong.")

        self.type = self.Type(cmd_type)
        self.para = cmd_body[3:]

        if self._type_check and self._type_check != self.type:
            raise TypeError("Unexpected message type %s" % self.type)

    @classmethod
    def parse(cls, strs):
        """Parse a json string and instanised a CMD subclass. 

        Parameters
        ----------
        strs : `bytes`
            Received `bytes` string.

        Returns
        -------
        `CMD.subclass`
            Instanised CMD subclass

        Raises
        ------
        NotImplementedError
            This subclass doesn't have a class implementation.
        """
        tmp = cls()
        try:
            tmp.unpack(strs)
        except ValueError as e:
            logger.exception(e)
            return None
        except TypeError as e:
            logger.exception(e)
            return None
            
        if tmp.type in cls._subclasses:
            tmp = cls._subclasses[tmp.type]()
            tmp.unpack(strs)
            return tmp
        else:
            raise NotImplementedError("No CMD subclass %s" % tmp.type)

    def __str__(self) -> str:
        return "<%s - %s>" % (self.type, self.para)

    @classmethod
    def add_handler(cls, type_, func, prior=False):
        """A classmethod called by workers, to bind their method to `type_` 
        CMD subclass. 

        `_handler` in the subclass present in list, so multiple
        methods can bind to the cmd class.

        Parameters
        ----------
        type_ : `CMD.Type`
            Which Type of `CMD` to bind to.
        func : `Func`
            Method to bind.
        prior : `bool`, optional
            If `True` instert this method to the first, means will call first in the handle proccess.
            If `False` will append this method to the last of the `_handler` list.
            by default False
        """
        subclass = cls._subclasses[type_]
        if prior:
            subclass._handlers = [func] + subclass._handlers
        else:
            subclass._handlers.append(func)



    async def __make_async(self, func):
        await asyncio.sleep(0)
        return func



    def pack_reply(self, resp_pld) -> bytes:       # Pack ID / func code / len
        if resp_pld is None:
            return None
        dev_id = (self.type.value >> 8)
        if(dev_id == 0x30):
            return pack(f'!HI{len(resp_pld)}H', self.type.value, len(resp_pld)*2 + 6, *resp_pld)
        else :
            return pack(f'!HH{len(resp_pld)}H', self.type.value, len(resp_pld)*2 + 4, *resp_pld)



    async def handle(self):
        """Launch all handlers in `_handler` list, and return their result list.

        Returns
        -------
        `List[results]`
            List of launched results.

        Raises
        ------
        NotImplementedError
            No handler bind to this type CMD
        """
        coros = []
        if not self._handlers:
            raise NotImplementedError(
                "No handler bind to CMD type %s" % self.type)
        else:
            for handler in self._handlers:
                if asyncio.iscoroutinefunction(handler):
                    coros += [handler(self)]
                else:
                    coros += [self.__make_async(handler(self))]
            res = await asyncio.gather(*coros)
            return res



@CMD.add_subclass(CMD.Type.GET_VERSION)
class CMD_GET_VERSION(CMD):
    _handlers = []

@CMD.add_subclass(CMD.Type.GET_SERIAL)
class CMD_GET_SERIAL(CMD):
    _handlers = []

@CMD.add_subclass(CMD.Type.GET_PARA)
class CMD_GET_PARA(CMD):
    _handlers = []

@CMD.add_subclass(CMD.Type.GET_SCAN_PARA)
class CMD_GET_SCAN_PARA(CMD):
    _handlers = []

@CMD.add_subclass(CMD.Type.GET_CH_CFG)
class CMD_GET_CH_CFG(CMD):
    _handlers = []
    @property
    def channel(self) -> int:
        return unpack('!BH', self.para)[0]
    @property
    def threshold(self) -> int:
        return unpack('!BH', self.para)[1]

@CMD.add_subclass(CMD.Type.SET_SCAN_PARA)  # XXX
class CMD_SET_SCAN_PARA(CMD):
    _handlers = []
    @property
    def start_pos(self) -> int:
        return unpack('!HHHHB', self.para)[0]
    @property
    def scan_step(self) -> int:     # ad step == 2GHz, scan
        return unpack('!HHHHB', self.para)[1]
    @property
    def stop_pos(self) -> int:
        return unpack('!HHHHB', self.para)[2]
    @property
    def spec_step(self) -> int:     # fix 1 GHz
        return 1

@CMD.add_subclass(CMD.Type.SET_THRESHOLD) 
class CMD_SET_THRESHOLD(CMD):
    _handlers = []
    @property
    def ch(self) -> int:
        return unpack('!BH', self.para)[0]
    @property
    def thresold(self) -> int:     
        return unpack('!BH', self.para)[1]

    
@CMD.add_subclass(CMD.Type.SET_AMPLIFY) # TODO
class CMD_SET_AMPLIFY(CMD):
    _handlers = []
    @property
    def ch(self) -> int:
        return unpack('!BBB', self.para)[0]
    @property
    def auto_set(self) -> bool:     
        return (unpack('!BBB', self.para)[1]  == 0x80)
    @property
    def amp(self) -> int:     
        return unpack('!BBB', self.para)[2] 
    

@CMD.add_subclass(CMD.Type.SET_PEAK_INTERVAL)   # TODO
class CMD_SET_PEAK_INTERVAL(CMD):
    _handlers = []


@CMD.add_subclass(CMD.Type.SAVE_THRESHOLD)
class CMD_SAVE_THRESHOLD(CMD):
    _handlers = []


@CMD.add_subclass(CMD.Type.STOP_SAMPLE)
class CMD_STOP_SAMPLE(CMD):
    _handlers = []


@CMD.add_subclass(CMD.Type.START_SAMPLE)
class CMD_START_SAMPLE(CMD):
    _handlers = []


@CMD.add_subclass(CMD.Type.START_TEST)  # TODO
class CMD_START_TEST(CMD):
    _handlers = []


@CMD.add_subclass(CMD.Type.START_SPEC_SAMPLE) # TODO
class CMD_START_SPEC_SAMPLE(CMD):
    _handlers = []

@CMD.add_subclass(CMD.Type.GET_ONESHOT_SPEC) 
class CMD_GET_ONESHOT_SPEC(CMD):
    _handlers = []
    @property
    def ch(self) -> int:
        return unpack('!BBB', self.para)[2]

@CMD.add_subclass(CMD.Type.SET_IIC) 
class CMD_SET_IIC(CMD):
    _handlers = []
    @property
    def i2c_ch(self) -> int:
        return unpack('!BBBBB', self.para)[0]
    @property
    def dev_addr(self) -> int:
        return unpack('!BBBBB', self.para)[1]
    @property
    def reg_addr(self) -> int:
        return unpack('!BBBBB', self.para)[2]
    @property
    def val(self) -> int:
        return unpack('!BBBBB', self.para)[3]


# @CMD.add_subclass(CMD.Type.SET_I2C)
# class CMD_SET_I2C(CMD):
#     _handlers = []
#     @property
#     def addr(self) -> int:
#         return unpack("!BB", self.para)[0]
#     @property
#     def val(self) -> int:
#         return unpack("!BB", self.para)[1]



# @CMD.add_subclass(CMD.Type.SET_CHANNEL_SWITCH)
# class CMD_SET_CHANNEL_SWITCH(CMD):
#     _handlers = []
#     @property
#     def channel_sel(self) -> CHANEL_SEL:
#         channel_sel_code = unpack("!BB", self.para)[0]
#         try:
#             return CHANEL_SEL(channel_sel_code)
#         except ValueError:
#             return CHANEL_SEL.CH8



# @CMD.add_subclass(CMD.Type.SET_SPEC_LENGTH)
# class CMD_SET_SPEC_LENGTH(CMD):
#     _handlers = []
#     @property
#     def spec_length(self) -> int:
#         return unpack("!H", self.para)[0]



# @CMD.add_subclass(CMD.Type.GET_VERSION)
# class CMD_GET_VERSION(CMD):
#     _handlers = []


# @CMD.add_subclass(CMD.Type.SAVE_CONFIG)
# class CMD_SAVE_CONFIG(CMD):
#     _handlers = []



# @CMD.add_subclass(CMD.Type.SET_IND_INTERVAL)  
# class CMD_SET_IND_INTERVAL(CMD):
#     _handlers = []
#     @property
#     def ind_interval(self) -> int:
#         return unpack('!I', self.para)[0]


# @CMD.add_subclass(CMD.Type.SET_START_SPEC)  
# class CMD_SET_START_SPEC(CMD):
#     _handlers = []
#     @property
#     def start_spec(self) -> int:
#         return unpack('!I', self.para)[0]


# @CMD.add_subclass(CMD.Type.SET_BULK_SIZE)  
# class CMD_SET_BULK_SIZE(CMD):
#     _handlers = []
#     @property
#     def bulk_size(self) -> int:
#         return unpack('!H', self.para)[0]






# @CMD.add_subclass(CMD.Type.SET_THRESHOLD_PER_POINT)  
# class CMD_SET_THRESHOLD_PER_POINT(CMD):
#     _handlers = []
#     @property
#     def channel(self) -> int:
#         return unpack(f'!B{MAX_SPEC_LENGTH}H', self.para)[0]
#     @property
#     def threshold_per_point(self) -> List[int]:
#         return list(unpack(f'!B{MAX_SPEC_LENGTH}H', self.para)[1:])



# @CMD.add_subclass(CMD.Type.GET_SPECTURE)
# class CMD_GET_SPECTURE(CMD):
#     _handlers = []
#     @property
#     def channel(self) -> bool:
#         return unpack('!BB', self.para)[0]



# @CMD.add_subclass(CMD.Type.SET_I2C) 
# class CMD_SET_I2C(CMD):
#     _handlers = []
#     @property
#     def i2c_ch(self) -> int:
#         return unpack('!BBBB', self.para)[0]
#     @property
#     def dev_addr(self) -> int:
#         return unpack('!BBBB', self.para)[1]
#     @property
#     def reg_addr(self) -> int:
#         return unpack('!BBBB', self.para)[2]
#     @property
#     def value(self) -> int:
#         return unpack('!BBBB', self.para)[3]



# @CMD.add_subclass(CMD.Type.GET_I2C) 
# class CMD_GET_I2C(CMD):
#     _handlers = []
#     @property
#     def i2c_ch(self) -> int:
#         return unpack('!BBB', self.para)[0]
#     @property
#     def dev_addr(self) -> int:
#         return unpack('!BBB', self.para)[1]
#     @property
#     def reg_addr(self) -> int:
#         return unpack('!BBB', self.para)[2]
